Project: Identify Customer Segments

In this project, you will apply unsupervised learning techniques to identify segments of the population that form the core customer base for a mail-order sales company in Germany. These segments can then be used to direct marketing campaigns towards audiences that will have the highest expected rate of returns. The data that you will use has been provided by our partners at Bertelsmann Arvato Analytics, and represents a real-life data science task.

This notebook will help you complete this task by providing a framework within which you will perform your analysis steps. In each step of the project, you will see some text describing the subtask that you will perform, followed by one or more code cells for you to complete your work. Feel free to add additional code and markdown cells as you go along so that you can explore everything in precise chunks. The code cells provided in the base template will outline only the major tasks, and will usually not be enough to cover all of the minor tasks that comprise it.

It should be noted that while there will be precise guidelines on how you should handle certain tasks in the project, there will also be places where an exact specification is not provided. There will be times in the project where you will need to make and justify your own decisions on how to treat the data. These are places where there may not be only one way to handle the data. In real-life tasks, there may be many valid ways to approach an analysis task. One of the most important things you can do is clearly document your approach so that other scientists can understand the decisions you've made.

At the end of most sections, there will be a Markdown cell labeled Discussion. In these cells, you will report your findings for the completed section, as well as document the decisions that you made in your approach to each subtask. Your project will be evaluated not just on the code used to complete the tasks outlined, but also your communication about your observations and conclusions at each stage.

In [1]:
# import libraries here; add more as necessary
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
init_notebook_mode(connected=True)
import ast
import plotly.graph_objs as go
from sklearn.preprocessing import Imputer, StandardScaler
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans


# magic word for producing visualizations in notebook
%matplotlib inline

Step 0: Load the Data

There are four files associated with this project (not including this one):

  • Udacity_AZDIAS_Subset.csv: Demographics data for the general population of Germany; 891211 persons (rows) x 85 features (columns).
  • Udacity_CUSTOMERS_Subset.csv: Demographics data for customers of a mail-order company; 191652 persons (rows) x 85 features (columns).
  • Data_Dictionary.md: Detailed information file about the features in the provided datasets.
  • AZDIAS_Feature_Summary.csv: Summary of feature attributes for demographics data; 85 features (rows) x 4 columns

Each row of the demographics files represents a single person, but also includes information outside of individuals, including information about their household, building, and neighborhood. You will use this information to cluster the general population into groups with similar demographic properties. Then, you will see how the people in the customers dataset fit into those created clusters. The hope here is that certain clusters are over-represented in the customers data, as compared to the general population; those over-represented clusters will be assumed to be part of the core userbase. This information can then be used for further applications, such as targeting for a marketing campaign.

To start off with, load in the demographics data for the general population into a pandas DataFrame, and do the same for the feature attributes summary. Note for all of the .csv data files in this project: they're semicolon (;) delimited, so you'll need an additional argument in your read_csv() call to read in the data properly. Also, considering the size of the main dataset, it may take some time for it to load completely.

Once the dataset is loaded, it's recommended that you take a little bit of time just browsing the general structure of the dataset and feature summary file. You'll be getting deep into the innards of the cleaning in the first major step of the project, so gaining some general familiarity can help you get your bearings.

In [2]:
# Load in the general demographics data.
azdias = pd.read_csv('Udacity_AZDIAS_Subset.csv', delimiter=';')

# Load in the feature summary file.
feat_info = pd.read_csv('AZDIAS_Feature_Summary.csv', delimiter=';')
In [3]:
# Check the structure of the data after it's loaded (e.g. print the number of
# rows and columns, print the first few rows).
rows = len(azdias)
columns = len(azdias.columns)

print('number of rows: {}'.format(rows))
print('number of columns: {}'.format(columns))
azdias.head()
number of rows: 891221
number of columns: 85
Out[3]:
AGER_TYP ALTERSKATEGORIE_GROB ANREDE_KZ CJT_GESAMTTYP FINANZ_MINIMALIST FINANZ_SPARER FINANZ_VORSORGER FINANZ_ANLEGER FINANZ_UNAUFFAELLIGER FINANZ_HAUSBAUER ... PLZ8_ANTG1 PLZ8_ANTG2 PLZ8_ANTG3 PLZ8_ANTG4 PLZ8_BAUMAX PLZ8_HHZ PLZ8_GBZ ARBEIT ORTSGR_KLS9 RELAT_AB
0 -1 2 1 2.0 3 4 3 5 5 3 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 -1 1 2 5.0 1 5 2 5 4 5 ... 2.0 3.0 2.0 1.0 1.0 5.0 4.0 3.0 5.0 4.0
2 -1 3 2 3.0 1 4 1 2 3 5 ... 3.0 3.0 1.0 0.0 1.0 4.0 4.0 3.0 5.0 2.0
3 2 4 2 2.0 4 2 5 2 1 2 ... 2.0 2.0 2.0 0.0 1.0 3.0 4.0 2.0 3.0 3.0
4 -1 3 1 5.0 4 3 4 1 3 2 ... 2.0 4.0 2.0 1.0 2.0 3.0 3.0 4.0 6.0 5.0

5 rows × 85 columns

In [4]:
azdias.columns
Out[4]:
Index(['AGER_TYP', 'ALTERSKATEGORIE_GROB', 'ANREDE_KZ', 'CJT_GESAMTTYP',
       'FINANZ_MINIMALIST', 'FINANZ_SPARER', 'FINANZ_VORSORGER',
       'FINANZ_ANLEGER', 'FINANZ_UNAUFFAELLIGER', 'FINANZ_HAUSBAUER',
       'FINANZTYP', 'GEBURTSJAHR', 'GFK_URLAUBERTYP', 'GREEN_AVANTGARDE',
       'HEALTH_TYP', 'LP_LEBENSPHASE_FEIN', 'LP_LEBENSPHASE_GROB',
       'LP_FAMILIE_FEIN', 'LP_FAMILIE_GROB', 'LP_STATUS_FEIN',
       'LP_STATUS_GROB', 'NATIONALITAET_KZ', 'PRAEGENDE_JUGENDJAHRE',
       'RETOURTYP_BK_S', 'SEMIO_SOZ', 'SEMIO_FAM', 'SEMIO_REL', 'SEMIO_MAT',
       'SEMIO_VERT', 'SEMIO_LUST', 'SEMIO_ERL', 'SEMIO_KULT', 'SEMIO_RAT',
       'SEMIO_KRIT', 'SEMIO_DOM', 'SEMIO_KAEM', 'SEMIO_PFLICHT', 'SEMIO_TRADV',
       'SHOPPER_TYP', 'SOHO_KZ', 'TITEL_KZ', 'VERS_TYP', 'ZABEOTYP',
       'ALTER_HH', 'ANZ_PERSONEN', 'ANZ_TITEL', 'HH_EINKOMMEN_SCORE',
       'KK_KUNDENTYP', 'W_KEIT_KIND_HH', 'WOHNDAUER_2008',
       'ANZ_HAUSHALTE_AKTIV', 'ANZ_HH_TITEL', 'GEBAEUDETYP', 'KONSUMNAEHE',
       'MIN_GEBAEUDEJAHR', 'OST_WEST_KZ', 'WOHNLAGE', 'CAMEO_DEUG_2015',
       'CAMEO_DEU_2015', 'CAMEO_INTL_2015', 'KBA05_ANTG1', 'KBA05_ANTG2',
       'KBA05_ANTG3', 'KBA05_ANTG4', 'KBA05_BAUMAX', 'KBA05_GBZ', 'BALLRAUM',
       'EWDICHTE', 'INNENSTADT', 'GEBAEUDETYP_RASTER', 'KKK', 'MOBI_REGIO',
       'ONLINE_AFFINITAET', 'REGIOTYP', 'KBA13_ANZAHL_PKW', 'PLZ8_ANTG1',
       'PLZ8_ANTG2', 'PLZ8_ANTG3', 'PLZ8_ANTG4', 'PLZ8_BAUMAX', 'PLZ8_HHZ',
       'PLZ8_GBZ', 'ARBEIT', 'ORTSGR_KLS9', 'RELAT_AB'],
      dtype='object')
In [5]:
feat_info
Out[5]:
attribute information_level type missing_or_unknown
0 AGER_TYP person categorical [-1,0]
1 ALTERSKATEGORIE_GROB person ordinal [-1,0,9]
2 ANREDE_KZ person categorical [-1,0]
3 CJT_GESAMTTYP person categorical [0]
4 FINANZ_MINIMALIST person ordinal [-1]
5 FINANZ_SPARER person ordinal [-1]
6 FINANZ_VORSORGER person ordinal [-1]
7 FINANZ_ANLEGER person ordinal [-1]
8 FINANZ_UNAUFFAELLIGER person ordinal [-1]
9 FINANZ_HAUSBAUER person ordinal [-1]
10 FINANZTYP person categorical [-1]
11 GEBURTSJAHR person numeric [0]
12 GFK_URLAUBERTYP person categorical []
13 GREEN_AVANTGARDE person categorical []
14 HEALTH_TYP person ordinal [-1,0]
15 LP_LEBENSPHASE_FEIN person mixed [0]
16 LP_LEBENSPHASE_GROB person mixed [0]
17 LP_FAMILIE_FEIN person categorical [0]
18 LP_FAMILIE_GROB person categorical [0]
19 LP_STATUS_FEIN person categorical [0]
20 LP_STATUS_GROB person categorical [0]
21 NATIONALITAET_KZ person categorical [-1,0]
22 PRAEGENDE_JUGENDJAHRE person mixed [-1,0]
23 RETOURTYP_BK_S person ordinal [0]
24 SEMIO_SOZ person ordinal [-1,9]
25 SEMIO_FAM person ordinal [-1,9]
26 SEMIO_REL person ordinal [-1,9]
27 SEMIO_MAT person ordinal [-1,9]
28 SEMIO_VERT person ordinal [-1,9]
29 SEMIO_LUST person ordinal [-1,9]
... ... ... ... ...
55 OST_WEST_KZ building categorical [-1]
56 WOHNLAGE building mixed [-1]
57 CAMEO_DEUG_2015 microcell_rr4 categorical [-1,X]
58 CAMEO_DEU_2015 microcell_rr4 categorical [XX]
59 CAMEO_INTL_2015 microcell_rr4 mixed [-1,XX]
60 KBA05_ANTG1 microcell_rr3 ordinal [-1]
61 KBA05_ANTG2 microcell_rr3 ordinal [-1]
62 KBA05_ANTG3 microcell_rr3 ordinal [-1]
63 KBA05_ANTG4 microcell_rr3 ordinal [-1]
64 KBA05_BAUMAX microcell_rr3 mixed [-1,0]
65 KBA05_GBZ microcell_rr3 ordinal [-1,0]
66 BALLRAUM postcode ordinal [-1]
67 EWDICHTE postcode ordinal [-1]
68 INNENSTADT postcode ordinal [-1]
69 GEBAEUDETYP_RASTER region_rr1 ordinal []
70 KKK region_rr1 ordinal [-1,0]
71 MOBI_REGIO region_rr1 ordinal []
72 ONLINE_AFFINITAET region_rr1 ordinal []
73 REGIOTYP region_rr1 ordinal [-1,0]
74 KBA13_ANZAHL_PKW macrocell_plz8 numeric []
75 PLZ8_ANTG1 macrocell_plz8 ordinal [-1]
76 PLZ8_ANTG2 macrocell_plz8 ordinal [-1]
77 PLZ8_ANTG3 macrocell_plz8 ordinal [-1]
78 PLZ8_ANTG4 macrocell_plz8 ordinal [-1]
79 PLZ8_BAUMAX macrocell_plz8 mixed [-1,0]
80 PLZ8_HHZ macrocell_plz8 ordinal [-1]
81 PLZ8_GBZ macrocell_plz8 ordinal [-1]
82 ARBEIT community ordinal [-1,9]
83 ORTSGR_KLS9 community ordinal [-1,0]
84 RELAT_AB community ordinal [-1,9]

85 rows × 4 columns

Tip: Add additional cells to keep everything in reasonably-sized chunks! Keyboard shortcut esc --> a (press escape to enter command mode, then press the 'A' key) adds a new cell before the active cell, and esc --> b adds a new cell after the active cell. If you need to convert an active cell to a markdown cell, use esc --> m and to convert to a code cell, use esc --> y.

Step 1: Preprocessing

Step 1.1: Assess Missing Data

The feature summary file contains a summary of properties for each demographics data column. You will use this file to help you make cleaning decisions during this stage of the project. First of all, you should assess the demographics data in terms of missing data. Pay attention to the following points as you perform your analysis, and take notes on what you observe. Make sure that you fill in the Discussion cell with your findings and decisions at the end of each step that has one!

Step 1.1.1: Convert Missing Value Codes to NaNs

The fourth column of the feature attributes summary (loaded in above as feat_info) documents the codes from the data dictionary that indicate missing or unknown data. While the file encodes this as a list (e.g. [-1,0]), this will get read in as a string object. You'll need to do a little bit of parsing to make use of it to identify and clean the data. Convert data that matches a 'missing' or 'unknown' value code into a numpy NaN value. You might want to see how much data takes on a 'missing' or 'unknown' code, and how much data is naturally missing, as a point of interest.

As one more reminder, you are encouraged to add additional cells to break up your analysis into manageable chunks.

In [6]:
# Identify missing or unknown data values and convert them to NaNs.

#grouping by missing or unkown types
feat_nan_types = feat_info[['missing_or_unknown',
                            'attribute']].groupby('missing_or_unknown')['attribute'].apply(list).reset_index()

#replacing values which cannot be converted to list by default
feat_nan_types['missing_or_unknown'].replace({'[-1,X]': '[-1,"X"]',
                                         '[-1,XX]': '[-1,"XX"]',
                                          '[XX]': '["XX"]'}, inplace = True)
#converting missing types to lists
feat_nan_types['missing_or_unknown'] = feat_nan_types['missing_or_unknown'].apply(lambda feat: ast.literal_eval(feat))

# filtering main dataframe and converting appropriate values to np.nan
for feat, attr in zip(feat_nan_types['missing_or_unknown'], feat_nan_types['attribute']):
    azdias[attr] = azdias[attr].applymap(lambda value: np.nan if value in feat else value)

Step 1.1.2: Assess Missing Data in Each Column

How much missing data is present in each column? There are a few columns that are outliers in terms of the proportion of values that are missing. You will want to use matplotlib's hist() function to visualize the distribution of missing value counts to find these columns. Identify and document these columns. While some of these columns might have justifications for keeping or re-encoding the data, for this project you should just remove them from the dataframe. (Feel free to make remarks about these outlier columns in the discussion, however!)

For the remaining features, are there any patterns in which columns have, or share, missing data?

In [7]:
# Perform an assessment of how much missing data there is in each column of the
# dataset.
feat_info['NULL_Value'] = feat_info['attribute'].apply(lambda attr: len(azdias[attr][azdias[attr].isnull()]))
feat_info['NULL_RATIO'] = feat_info['attribute'].apply(lambda attr:
                                                       len(azdias[attr][azdias[attr].isnull()]) / len(azdias[attr]) )
In [8]:
# Investigate patterns in the amount of missing data in each column.



feat_info.sort_values('NULL_RATIO' , ascending=False, inplace=True)
data = [go.Bar(
            x=feat_info['attribute'],
            y=feat_info['NULL_RATIO']
    )]

layout= dict(title = 'Distribution of null value ratios for all columns',
              xaxis = dict(title = 'Attributes'),
              yaxis = dict(title = 'Null Value Ratio'))
fig = go.Figure(data=data, layout=layout)

iplot(fig)
In [9]:
levels = list(feat_info['information_level'].drop_duplicates())

for level in levels:
    data = [go.Bar(
                    x=feat_info['attribute'][feat_info['information_level']==level],
                    y=feat_info['NULL_Value'][feat_info['information_level']==level]
            )]

    layout= dict(title = 'Distribution of null value rows for all columns within {} feature group'.format(level),
              xaxis = dict(title = 'Attributes'),
              yaxis = dict(title = 'Null Value count'))
    fig = go.Figure(data=data, layout=layout)

    iplot(fig)
In [10]:
# Remove the outlier columns from the dataset. (You'll perform other data
# engineering tasks such as re-encoding and imputation later.)
feat_info_columns = feat_info['attribute'][feat_info['NULL_RATIO']<0.18].values.tolist()
In [11]:
azdias = azdias[feat_info_columns].copy()

Discussion 1.1.2: Assess Missing Data in Each Column

(Double click this cell and replace this text with your own text, reporting your observations regarding the amount of missing data in each column. Are there any patterns in missing values? Which columns were removed from the dataset?)

There are six columns where the ratio of the Null values was above 18 % of the total row count in the column. These were removed . The column names are: 'TITEL_KZ', 'AGER_TYP', 'KK_KUNDENTYP', 'KBA05_BAUMAX', 'GEBURTSJAHR', 'ALTER_HH' For the rest of the columns while null values are present, removing them would probably cause to information loss.

In terms of patterns for the missing values we can observe that the ratio of null values show similar characteristics for the attributes within their particular information level groups. From this we can assume that the data have been collected and assebled by feature level

Step 1.1.3: Assess Missing Data in Each Row

Now, you'll perform a similar assessment for the rows of the dataset. How much data is missing in each row? As with the columns, you should see some groups of points that have a very different numbers of missing values. Divide the data into two subsets: one for data points that are above some threshold for missing values, and a second subset for points below that threshold.

In order to know what to do with the outlier rows, we should see if the distribution of data values on columns that are not missing data (or are missing very little data) are similar or different between the two groups. Select at least five of these columns and compare the distribution of values.

  • You can use seaborn's countplot() function to create a bar chart of code frequencies and matplotlib's subplot() function to put bar charts for the two subplots side by side.
  • To reduce repeated code, you might want to write a function that can perform this comparison, taking as one of its arguments a column to be compared.

Depending on what you observe in your comparison, this will have implications on how you approach your conclusions later in the analysis. If the distributions of non-missing features look similar between the data with many missing values and the data with few or no missing values, then we could argue that simply dropping those points from the analysis won't present a major issue. On the other hand, if the data with many missing values looks very different from the data with few or no missing values, then we should make a note on those data as special. We'll revisit these data later on. Either way, you should continue your analysis for now using just the subset of the data with few or no missing values.

In [12]:
# How much data is missing in each row of the dataset?
azdias['missing_value'] = azdias.isnull().sum(axis=1) 
In [13]:
data = [go.Histogram(x=azdias['missing_value'])]
layout= dict(title = 'Distribution of null value count within rows',
              xaxis = dict(title = 'Number of rows containing null values'),
              yaxis = dict(title = 'Value count'))
fig = go.Figure(data=data, layout=layout)

iplot(fig)
In [14]:
# Write code to divide the data into two subsets based on the number of missing
# values in each row.
azdias_1 = azdias[azdias['missing_value'] <= 15].copy()
azdias_2 = azdias[azdias['missing_value'] > 15].copy()
In [15]:
def plot_distribution(column):
    
    trace1 = go.Histogram(x=azdias_1[column],
                          name='missing_value <= 15'
                         
                         
                         )
    
    trace2 = go.Histogram(x=azdias_2[column],
                         name='missing_value >= 15'
                         
                         )
    
    data = [trace1, trace2]
    
    layout= dict(title = 'Distribution of values for two subset for column {}'.format(column),
              xaxis = dict(title = 'values in column'),
              yaxis = dict(title = 'Values'))
    
    fig = go.Figure(data=data, layout=layout)

    iplot(fig)
    
    
In [16]:
# Compare the distribution of values for at least five columns where there are
# no or few missing values, between the two subsets.

for col in ['ANREDE_KZ','GREEN_AVANTGARDE','FINANZTYP','SEMIO_FAM','HH_EINKOMMEN_SCORE','LP_STATUS_FEIN']:
    plot_distribution(col)

Discussion 1.1.3: Assess Missing Data in Each Row

(Double-click this cell and replace this text with your own text, reporting your observations regarding missing data in rows. Are the data with lots of missing values are qualitatively different from data with few or no missing values?)

Based on the distribution of the values above we can say that the two subset is qualitatively different, and we cannot simply remove it from the dataset, rather we would need to handle it in a special way.

Step 1.2: Select and Re-Encode Features

Checking for missing data isn't the only way in which you can prepare a dataset for analysis. Since the unsupervised learning techniques to be used will only work on data that is encoded numerically, you need to make a few encoding changes or additional assumptions to be able to make progress. In addition, while almost all of the values in the dataset are encoded using numbers, not all of them represent numeric values. Check the third column of the feature summary (feat_info) for a summary of types of measurement.

  • For numeric and interval data, these features can be kept without changes.
  • Most of the variables in the dataset are ordinal in nature. While ordinal values may technically be non-linear in spacing, make the simplifying assumption that the ordinal variables can be treated as being interval in nature (that is, kept without any changes).
  • Special handling may be necessary for the remaining two variable types: categorical, and 'mixed'.

In the first two parts of this sub-step, you will perform an investigation of the categorical and mixed-type features and make a decision on each of them, whether you will keep, drop, or re-encode each. Then, in the last part, you will create a new data frame with only the selected and engineered columns.

Data wrangling is often the trickiest part of the data analysis process, and there's a lot of it to be done here. But stick with it: once you're done with this step, you'll be ready to get to the machine learning parts of the project!

In [17]:
# How many features are there of each data type?
type_count = feat_info[['attribute', 'type']].groupby('type').count().reset_index().sort_values('attribute', ascending=False)

data = [go.Bar(
            x=type_count['type'],
            y=type_count['attribute']
    )]



layout= dict(title = 'Number of features for each data type',
              xaxis = dict(title = 'Data Types'),
              yaxis = dict(title = 'Number of features'))

fig = go.Figure(data=data, layout=layout)

iplot(fig)

Step 1.2.1: Re-Encode Categorical Features

For categorical data, you would ordinarily need to encode the levels as dummy variables. Depending on the number of categories, perform one of the following:

  • For binary (two-level) categoricals that take numeric values, you can keep them without needing to do anything.
  • There is one binary variable that takes on non-numeric values. For this one, you need to re-encode the values as numbers or create a dummy variable.
  • For multi-level categoricals (three or more values), you can choose to encode the values using multiple dummy variables (e.g. via OneHotEncoder), or (to keep things straightforward) just drop them from the analysis. As always, document your choices in the Discussion section.
In [18]:
# Assess categorical variables: which are binary, which are multi-level, and
# which one needs to be re-encoded?

feat_need = list(feat_info['attribute'][(feat_info['type'] == 'categorical') & (feat_info['NULL_RATIO']<=0.18)])
for col in feat_need:
    print(azdias_1[col].drop_duplicates())
1     2.0
2     1.0
34    NaN
Name: VERS_TYP, dtype: float64
1     3.0
2     2.0
3     1.0
5     0.0
34    NaN
Name: SHOPPER_TYP, dtype: float64
1     1.0
16    3.0
18    2.0
34    NaN
Name: NATIONALITAET_KZ, dtype: float64
1       8A
2       4C
3       2A
4       6B
5       8C
6       4A
7       2D
8       1A
9       1E
10      9D
13      5C
15      8B
16      7A
19      5D
21      9E
27      9B
32      1B
34      3D
39     NaN
41      4E
42      4B
44      3C
51      5A
63      7B
64      9A
72      6D
82      6E
85      2C
96      7C
104     9C
113     7D
117     5E
124     1D
126     8D
129     6C
136     6A
155     5B
162     4D
169     3A
172     2B
192     7E
296     3B
323     6F
400     5F
421     1C
Name: CAMEO_DEU_2015, dtype: object
1       8
2       4
3       2
4       6
8       1
10      9
13      5
16      7
34      3
39    NaN
Name: CAMEO_DEUG_2015, dtype: object
1      W
197    O
Name: OST_WEST_KZ, dtype: object
1         8.0
2         1.0
8         3.0
16        2.0
80        6.0
1371      4.0
882990    5.0
Name: GEBAEUDETYP, dtype: float64
1       5.0
2       1.0
3       NaN
4      10.0
19      2.0
27      7.0
33     11.0
66      8.0
71      4.0
82      6.0
323     9.0
391     3.0
Name: LP_FAMILIE_FEIN, dtype: float64
1     3.0
2     1.0
3     NaN
4     5.0
19    2.0
27    4.0
Name: LP_FAMILIE_GROB, dtype: float64
1    1.0
2    0.0
Name: SOHO_KZ, dtype: float64
1      10.0
3       1.0
4       5.0
6      12.0
7       9.0
8       3.0
10      8.0
18     11.0
22      4.0
45      7.0
64      2.0
68      6.0
184     NaN
Name: GFK_URLAUBERTYP, dtype: float64
1      5.0
2      3.0
3      2.0
9      4.0
10     1.0
12     6.0
184    NaN
Name: CJT_GESAMTTYP, dtype: float64
1       2.0
2       3.0
3       9.0
5       4.0
7       1.0
8      10.0
13      5.0
15      8.0
80      6.0
113     7.0
184     NaN
Name: LP_STATUS_FEIN, dtype: float64
1      1.0
2      2.0
3      4.0
8      5.0
80     3.0
184    NaN
Name: LP_STATUS_GROB, dtype: float64
1     1
3     6
4     5
5     2
6     4
12    3
Name: FINANZTYP, dtype: int64
1    0
2    1
Name: GREEN_AVANTGARDE, dtype: int64
1    2
4    1
Name: ANREDE_KZ, dtype: int64
1     5
3     3
4     4
7     1
8     6
19    2
Name: ZABEOTYP, dtype: int64
In [19]:
# Re-encode categorical variable(s) to be kept in the analysis.
#converting binary categorical variable to 1 and 0
azdias_1['OST_WEST_KZ'] = azdias_1['OST_WEST_KZ'].replace({'W':1, 'O':0})
feat_need.remove('OST_WEST_KZ')
In [20]:
encoded = pd.get_dummies(azdias_1[feat_need])
cols_to_use = encoded.columns.difference(azdias_1.columns)
azdias_1 = pd.merge(azdias_1, encoded[cols_to_use], left_index=True, right_index=True, how='left')

Discussion 1.2.1: Re-Encode Categorical Features

(Double-click this cell and replace this text with your own text, reporting your findings and decisions regarding categorical features. Which ones did you keep, which did you drop, and what engineering steps did you perform?) 'AGER_TYP', 'KK_KUNDENTYP', 'TITEL_KZ' categorical features have been already disclosed from the processing of the data due to the high ratio of NULL values.Based on this I've not examined these columns from an encoding perspective. The changes that I've made are the following:

Step 1.2.2: Engineer Mixed-Type Features

There are a handful of features that are marked as "mixed" in the feature summary that require special treatment in order to be included in the analysis. There are two in particular that deserve attention; the handling of the rest are up to your own choices:

  • "PRAEGENDE_JUGENDJAHRE" combines information on three dimensions: generation by decade, movement (mainstream vs. avantgarde), and nation (east vs. west). While there aren't enough levels to disentangle east from west, you should create two new variables to capture the other two dimensions: an interval-type variable for decade, and a binary variable for movement.
  • "CAMEO_INTL_2015" combines information on two axes: wealth and life stage. Break up the two-digit codes by their 'tens'-place and 'ones'-place digits into two new ordinal variables (which, for the purposes of this project, is equivalent to just treating them as their raw numeric values).
  • If you decide to keep or engineer new features around the other mixed-type features, make sure you note your steps in the Discussion section.

Be sure to check Data_Dictionary.md for the details needed to finish these tasks.

In [21]:
# Investigate "PRAEGENDE_JUGENDJAHRE" and engineer two new variables.
full_map = pd.DataFrame({-1: [np.nan, np.nan],
                            0: [np.nan, np.nan],
                              1: [40, 1],
                              2: [40, 0],
                              3: [50, 1],
                              4: [50, 0],
                              5: [60, 1],
                              6: [60, 0],
                              7: [60, 0],
                              8: [70, 1],
                              9: [70, 0],
                              10: [80, 1],
                              11: [80, 0],
                              12: [80, 1],
                              13: [80, 0],
                              14: [90, 1],
                              15: [90, 0],
                              np.nan: [np.nan, np.nan]}).T
full_map.reset_index(inplace=True)
full_map.columns = ['PRAEGENDE_JUGENDJAHRE', 'DECADES', 'MOVEMENTS']
azdias_1 = pd.merge(azdias_1, full_map, how='left', on='PRAEGENDE_JUGENDJAHRE')
In [22]:
# Investigate "CAMEO_INTL_2015" and engineer two new variables.
azdias_1['WEALTH'] = pd.to_numeric(azdias_1['CAMEO_INTL_2015'])//10
azdias_1['LIFE_STAGE'] = pd.to_numeric(azdias_1['CAMEO_INTL_2015'])%10
In [23]:
azdias_1['RURAL'] = pd.cut(azdias_1['WOHNLAGE'], [1,6,9], False, [0,1])
azdias_1['NEIGHBORHOOD_QUAL'] = azdias_1['WOHNLAGE'].replace([7,8], np.nan)

Discussion 1.2.2: Engineer Mixed-Type Features

(Double-click this cell and replace this text with your own text, reporting your findings and decisions regarding mixed-value features. Which ones did you keep, which did you drop, and what engineering steps did you perform?)

The two columns 'PRAEGENDE_JUGENDJAHRE' and 'CAMEO_INTL_2015' have been modified to showcase the multiple dimensionality of the features. The rest of the mixed features have been left as it is. WOHNLAGE have been modified to properly represent the buidling quality (1-5) and if a neighbourhood is rural or not (7-8). The rest of the mixed type variables (PLZ8_BAUMAX, LPLEBENSPHASE_FEIN , LP_LEBENSPHASE_GROB) are dropped.

Step 1.2.3: Complete Feature Selection

In order to finish this step up, you need to make sure that your data frame now only has the columns that you want to keep. To summarize, the dataframe should consist of the following:

  • All numeric, interval, and ordinal type columns from the original dataset.
  • Binary categorical features (all numerically-encoded).
  • Engineered features from other multi-level categorical features and mixed features.

Make sure that for any new columns that you have engineered, that you've excluded the original columns from the final dataset. Otherwise, their values will interfere with the analysis later on the project. For example, you should not keep "PRAEGENDE_JUGENDJAHRE", since its values won't be useful for the algorithm: only the values derived from it in the engineered features you created should be retained. As a reminder, your data should only be from the subset with few or no missing values.

In [ ]:
# If there are other re-engineering tasks you need to perform, make sure you
# take care of them here. (Dealing with missing data will come in step 2.1.)
In [24]:
# Do whatever you need to in order to ensure that the dataframe only contains

#deleting unnecessary columns
del(azdias_1['PRAEGENDE_JUGENDJAHRE'])
del(azdias_1['CAMEO_INTL_2015'])
del(azdias_1['CAMEO_DEUG_2015'])
del(azdias_1['CAMEO_DEU_2015'])
del(azdias_1['missing_value'])
del(azdias_1['WOHNLAGE'])
del(azdias_1['PLZ8_BAUMAX'])
del(azdias_1['LP_LEBENSPHASE_FEIN'])
del(azdias_1['LP_LEBENSPHASE_GROB'])

Step 1.3: Create a Cleaning Function

Even though you've finished cleaning up the general population demographics data, it's important to look ahead to the future and realize that you'll need to perform the same cleaning steps on the customer demographics data. In this substep, complete the function below to execute the main feature selection, encoding, and re-engineering steps you performed above. Then, when it comes to looking at the customer data in Step 3, you can just run this function on that DataFrame to get the trimmed dataset in a single step.

In [25]:
def clean_data(df):
    """
    Perform feature trimming, re-encoding, and engineering for demographics
    data
    
    INPUT: Demographics DataFrame
    OUTPUT: Trimmed and cleaned demographics DataFrame
    """
    
    # Put in code here to execute all main cleaning steps:
    # convert missing value codes into NaNs, ...
    
    
    feat_nan_types = feat_info[['missing_or_unknown',
                            'attribute']].groupby('missing_or_unknown')['attribute'].apply(list).reset_index()

    #replacing values which cannot be converted to list by default
    feat_nan_types['missing_or_unknown'].replace({'[-1,X]': '[-1,"X"]',
                                             '[-1,XX]': '[-1,"XX"]',
                                              '[XX]': '["XX"]'}, inplace = True)
    #converting missing types to lists
    feat_nan_types['missing_or_unknown'] = feat_nan_types['missing_or_unknown'].apply(lambda feat: ast.literal_eval(feat))
    
    for feat, attr in zip(feat_nan_types['missing_or_unknown'], feat_nan_types['attribute']):
        df[attr] = df[attr].applymap(lambda value: np.nan if value in feat else value)
        
        
    feat_info['NULL_Value'] = feat_info['attribute'].apply(lambda attr: len(azdias[attr][azdias[attr].isnull()]))
    feat_info['NULL_RATIO'] = feat_info['attribute'].apply(lambda attr:
                                                       len(azdias[attr][azdias[attr].isnull()]) / len(azdias[attr]) )
    
    
    # remove selected columns and rows, ...
    
    feat_info_columns = feat_info['attribute'][feat_info['NULL_RATIO']<0.18].values.tolist()
    
    df = df[feat_info_columns].copy()
    
    df['missing_value'] = df.isnull().sum(axis=1) 
    
    
    df_1 = df[df['missing_value'] <= 15].copy()
    
    
    # select, re-encode, and engineer column values.
    
    df_1['OST_WEST_KZ'] = df_1['OST_WEST_KZ'].replace({'W':1, 'O':0})
    
    feat_need = list(feat_info['attribute'][(feat_info['type'] == 'categorical') & (feat_info['NULL_RATIO']<=0.18)])
    feat_need.remove('OST_WEST_KZ')
    
    encoded = pd.get_dummies(df_1[feat_need])
    
    cols_to_use = encoded.columns.difference(df_1.columns)
    
    df_1 = pd.merge(df_1, encoded[cols_to_use], left_index=True, right_index=True, how='left')
    
    
    full_map = pd.DataFrame({-1: [np.nan, np.nan],
                            0: [np.nan, np.nan],
                              1: [40, 1],
                              2: [40, 0],
                              3: [50, 1],
                              4: [50, 0],
                              5: [60, 1],
                              6: [60, 0],
                              7: [60, 0],
                              8: [70, 1],
                              9: [70, 0],
                              10: [80, 1],
                              11: [80, 0],
                              12: [80, 1],
                              13: [80, 0],
                              14: [90, 1],
                              15: [90, 0],
                              np.nan: [np.nan, np.nan]}).T
    
    full_map.reset_index(inplace=True)
    full_map.columns = ['PRAEGENDE_JUGENDJAHRE', 'DECADES', 'MOVEMENTS']
    df_1 = pd.merge(df_1, full_map, how='left', on='PRAEGENDE_JUGENDJAHRE')
    
    
    df_1['WEALTH'] = pd.to_numeric(df_1['CAMEO_INTL_2015'])//10
    df_1['LIFE_STAGE'] = pd.to_numeric(df_1['CAMEO_INTL_2015'])%10
    
    df_1['RURAL'] = pd.cut(df_1['WOHNLAGE'], [1,6,9], False, [0,1])
    df_1['NEIGHBORHOOD_QUAL'] = df_1['WOHNLAGE'].replace([7,8], np.nan)
    
    del(df_1['PRAEGENDE_JUGENDJAHRE'])
    del(df_1['CAMEO_INTL_2015'])
    del(df_1['CAMEO_DEUG_2015'])
    del(df_1['CAMEO_DEU_2015'])
    del(df_1['missing_value'])
    del(df_1['WOHNLAGE'])
    del(df_1['PLZ8_BAUMAX'])
    del(df_1['LP_LEBENSPHASE_FEIN'])
    del(df_1['LP_LEBENSPHASE_GROB'])
        
    df_1 = df_1.applymap(lambda x: x.strip() if type(x) is str else x)

    
    # Return the cleaned dataframe.
        
    return df_1
    
    
In [26]:
azdias = pd.read_csv('Udacity_AZDIAS_Subset.csv', delimiter=';')
In [27]:
azdias_cln = clean_data(azdias)
In [28]:
feature_names = list(azdias_cln.columns)

Step 2: Feature Transformation

Step 2.1: Apply Feature Scaling

Before we apply dimensionality reduction techniques to the data, we need to perform feature scaling so that the principal component vectors are not influenced by the natural differences in scale for features. Starting from this part of the project, you'll want to keep an eye on the API reference page for sklearn to help you navigate to all of the classes and functions that you'll need. In this substep, you'll need to check the following:

  • sklearn requires that data not have missing values in order for its estimators to work properly. So, before applying the scaler to your data, make sure that you've cleaned the DataFrame of the remaining missing values. This can be as simple as just removing all data points with missing data, or applying an Imputer to replace all missing values. You might also try a more complicated procedure where you temporarily remove missing values in order to compute the scaling parameters before re-introducing those missing values and applying imputation. Think about how much missing data you have and what possible effects each approach might have on your analysis, and justify your decision in the discussion section below.
  • For the actual scaling function, a StandardScaler instance is suggested, scaling each feature to mean 0 and standard deviation 1.
  • For these classes, you can make use of the .fit_transform() method to both fit a procedure to the data as well as apply the transformation to the data at the same time. Don't forget to keep the fit sklearn objects handy, since you'll be applying them to the customer demographics data towards the end of the project.
In [29]:
# If you've not yet cleaned the dataset of all NaN values, then investigate and
# do that now.

imputer = Imputer()
imputer_model = imputer.fit(azdias_cln)
azdias_impute = pd.DataFrame(imputer_model.transform(azdias_cln), columns = feature_names)
C:\Users\HUH82493\AppData\Local\Continuum\Anaconda3\lib\site-packages\sklearn\utils\deprecation.py:58: DeprecationWarning:

Class Imputer is deprecated; Imputer was deprecated in version 0.20 and will be removed in 0.22. Import impute.SimpleImputer from sklearn instead.

In [30]:
# Apply feature scaling to the general population demographics data.

scalar = StandardScaler()
scalar_model = scalar.fit(azdias_impute)
azdias_scaled = pd.DataFrame(scalar_model.transform(azdias_impute), columns = feature_names)

Discussion 2.1: Apply Feature Scaling

(Double-click this cell and replace this text with your own text, reporting your decisions regarding feature scaling.) I've used sklearn imputer function to remove the remaining NaN values, after this I've applied the StandardScaler to normalize the values

Step 2.2: Perform Dimensionality Reduction

On your scaled data, you are now ready to apply dimensionality reduction techniques.

  • Use sklearn's PCA class to apply principal component analysis on the data, thus finding the vectors of maximal variance in the data. To start, you should not set any parameters (so all components are computed) or set a number of components that is at least half the number of features (so there's enough features to see the general trend in variability).
  • Check out the ratio of variance explained by each principal component as well as the cumulative variance explained. Try plotting the cumulative or sequential values using matplotlib's plot() function. Based on what you find, select a value for the number of transformed features you'll retain for the clustering part of the project.
  • Once you've made a choice for the number of components to keep, make sure you re-fit a PCA instance to perform the decided-on transformation.
In [31]:
# Apply PCA to the data.

pca = PCA()
pca_model= pca.fit(azdias_scaled)
azdias_pca = pca_model.transform(azdias_scaled)
In [32]:
# Investigate the variance accounted for by each principal component.
num_components = len(pca.explained_variance_ratio_)
ind = np.arange(num_components)
vals = pca.explained_variance_ratio_
cumvals = np.cumsum(vals)



trace1 = go.Bar(x=ind,
                y=vals)
    
trace2 = go.Scatter(
    x = ind,
    y = cumvals,
    mode='lines',

)

layout= dict(title = 'Explained Variance per principal component',
              xaxis = dict(title = 'Principal Component'),
              yaxis = dict(title = 'Variance explained (%)'),
              )
data = [trace1, trace2]

fig = go.Figure(data=data, layout=layout)
    
iplot(fig)
In [33]:
# Re-apply PCA to the data while selecting for number of components to retain.
pca = PCA(n_components=70)
azdias_pca = pca.fit_transform(azdias_scaled)

Discussion 2.2: Perform Dimensionality Reduction

(Double-click this cell and replace this text with your own text, reporting your findings and decisions regarding dimensionality reduction. How many principal components / transformed features are you retaining for the next step of the analysis?)
I'm retaining 70 components, due to the above plot this component amount will represent at least 95% of the total variances.

Step 2.3: Interpret Principal Components

Now that we have our transformed principal components, it's a nice idea to check out the weight of each variable on the first few components to see if they can be interpreted in some fashion.

As a reminder, each principal component is a unit vector that points in the direction of highest variance (after accounting for the variance captured by earlier principal components). The further a weight is from zero, the more the principal component is in the direction of the corresponding feature. If two features have large weights of the same sign (both positive or both negative), then increases in one tend expect to be associated with increases in the other. To contrast, features with different signs can be expected to show a negative correlation: increases in one variable should result in a decrease in the other.

  • To investigate the features, you should map each weight to their corresponding feature name, then sort the features according to weight. The most interesting features for each principal component, then, will be those at the beginning and end of the sorted list. Use the data dictionary document to help you understand these most prominent features, their relationships, and what a positive or negative value on the principal component might indicate.
  • You should investigate and interpret feature associations from the first three principal components in this substep. To help facilitate this, you should write a function that you can call at any time to print the sorted list of feature weights, for the i-th principal component. This might come in handy in the next step of the project, when you interpret the tendencies of the discovered clusters.
In [34]:
# Map weights for the first principal component to corresponding feature names
# and then print the linked values, sorted by weight.
# HINT: Try defining a function here or in a new cell that you can reuse in the
# other cells.
def get_pca_weights(pca, component_number, feature_names):

    component_linkage = pd.DataFrame(pca.components_, columns = feature_names)
    relevancy = pd.DataFrame(component_linkage.iloc[component_number - 1])
    relevancy.columns = ['weights']
    relevancy.reset_index(inplace=True)
    relevancy.sort_values('weights', ascending=False, inplace=True)
    
    return relevancy
In [35]:
pca_1 = get_pca_weights(pca_model, 1, feature_names)
pca_1
Out[35]:
index weights
126 WEALTH 0.199974
9 PLZ8_ANTG3 0.197513
39 HH_EINKOMMEN_SCORE 0.193949
14 PLZ8_ANTG4 0.190515
23 ORTSGR_KLS9 0.169559
26 EWDICHTE 0.167560
49 FINANZ_HAUSBAUER 0.157799
10 PLZ8_ANTG2 0.135772
6 KBA05_ANTG4 0.135190
70 ZABEOTYP 0.126358
21 ARBEIT 0.126010
5 KBA05_ANTG3 0.122437
20 ANZ_HAUSHALTE_AKTIV 0.121862
79 CAMEO_DEUG_2015_9 0.121353
22 RELAT_AB 0.118630
51 FINANZ_SPARER 0.105118
125 MOVEMENTS 0.105057
78 CAMEO_DEUG_2015_8 0.100410
129 NEIGHBORHOOD_QUAL 0.077430
58 SEMIO_PFLICHT 0.076455
60 SEMIO_REL 0.072042
115 CAMEO_DEU_2015_8A 0.068717
1 REGIOTYP 0.068610
31 GEBAEUDETYP 0.066404
124 DECADES 0.062731
66 SEMIO_RAT 0.062451
122 CAMEO_DEU_2015_9D 0.061623
121 CAMEO_DEU_2015_9C 0.061493
120 CAMEO_DEU_2015_9B 0.060788
40 GFK_URLAUBERTYP 0.057396
... ... ...
87 CAMEO_DEU_2015_2C -0.049399
95 CAMEO_DEU_2015_4C -0.049951
71 CAMEO_DEUG_2015_1 -0.050948
88 CAMEO_DEU_2015_2D -0.053099
30 MIN_GEBAEUDEJAHR -0.053476
45 ONLINE_AFFINITAET -0.061851
19 KBA13_ANZAHL_PKW -0.067319
73 CAMEO_DEUG_2015_3 -0.070971
53 FINANZ_VORSORGER -0.074671
46 ALTERSKATEGORIE_GROB -0.075960
74 CAMEO_DEUG_2015_4 -0.078658
32 LP_FAMILIE_FEIN -0.081925
38 ANZ_PERSONEN -0.083534
33 LP_FAMILIE_GROB -0.083865
72 CAMEO_DEUG_2015_2 -0.095340
28 GEBAEUDETYP_RASTER -0.101161
27 BALLRAUM -0.104649
50 GREEN_AVANTGARDE -0.107281
127 LIFE_STAGE -0.116508
128 RURAL -0.123444
25 INNENSTADT -0.138538
11 PLZ8_GBZ -0.145041
34 KONSUMNAEHE -0.146140
52 FINANZ_MINIMALIST -0.190162
7 KBA05_GBZ -0.193824
13 PLZ8_ANTG1 -0.197624
3 KBA05_ANTG1 -0.203340
43 LP_STATUS_GROB -0.210993
42 LP_STATUS_FEIN -0.213353
8 MOBI_REGIO -0.217389

130 rows × 2 columns

In [36]:
# Map weights for the second principal component to corresponding feature names
# and then print the linked values, sorted by weight.

pca_2 = get_pca_weights(pca_model, 2, feature_names)
pca_2
Out[36]:
index weights
46 ALTERSKATEGORIE_GROB 0.265144
53 FINANZ_VORSORGER 0.238474
64 SEMIO_ERL 0.222119
63 SEMIO_LUST 0.185255
44 RETOURTYP_BK_S 0.162814
2 W_KEIT_KIND_HH 0.119443
48 FINANZTYP 0.116953
67 SEMIO_KRIT 0.100369
17 SHOPPER_TYP 0.100338
52 FINANZ_MINIMALIST 0.099115
56 SEMIO_KAEM 0.088806
49 FINANZ_HAUSBAUER 0.087214
55 ANREDE_KZ 0.070854
35 WOHNDAUER_2008 0.063365
26 EWDICHTE 0.060982
23 ORTSGR_KLS9 0.059885
9 PLZ8_ANTG3 0.059231
14 PLZ8_ANTG4 0.055462
68 SEMIO_DOM 0.053668
126 WEALTH 0.049165
21 ARBEIT 0.048260
6 KBA05_ANTG4 0.044627
22 RELAT_AB 0.044333
20 ANZ_HAUSHALTE_AKTIV 0.042300
123 CAMEO_DEU_2015_9E 0.041882
10 PLZ8_ANTG2 0.041194
118 CAMEO_DEU_2015_8D 0.037258
78 CAMEO_DEUG_2015_8 0.034806
39 HH_EINKOMMEN_SCORE 0.026859
15 VERS_TYP 0.026759
... ... ...
30 MIN_GEBAEUDEJAHR -0.040603
40 GFK_URLAUBERTYP -0.043372
34 KONSUMNAEHE -0.044414
3 KBA05_ANTG1 -0.044993
11 PLZ8_GBZ -0.046408
128 RURAL -0.046502
62 SEMIO_VERT -0.047966
25 INNENSTADT -0.048206
8 MOBI_REGIO -0.052138
7 KBA05_GBZ -0.053919
13 PLZ8_ANTG1 -0.057278
38 ANZ_PERSONEN -0.058907
16 HEALTH_TYP -0.059417
32 LP_FAMILIE_FEIN -0.066152
33 LP_FAMILIE_GROB -0.067227
18 NATIONALITAET_KZ -0.075475
57 SEMIO_SOZ -0.092771
41 CJT_GESAMTTYP -0.127600
45 ONLINE_AFFINITAET -0.160212
61 SEMIO_MAT -0.160551
59 SEMIO_FAM -0.175413
66 SEMIO_RAT -0.181760
65 SEMIO_KULT -0.208751
54 FINANZ_ANLEGER -0.210988
47 FINANZ_UNAUFFAELLIGER -0.232261
69 SEMIO_TRADV -0.232912
58 SEMIO_PFLICHT -0.234475
51 FINANZ_SPARER -0.248466
60 SEMIO_REL -0.254787
124 DECADES -0.257056

130 rows × 2 columns

In [37]:
# Map weights for the third principal component to corresponding feature names
# and then print the linked values, sorted by weight.
pca_3 = get_pca_weights(pca_model, 3, feature_names)
pca_3
Out[37]:
index weights
62 SEMIO_VERT 0.337453
57 SEMIO_SOZ 0.260932
59 SEMIO_FAM 0.255288
65 SEMIO_KULT 0.243431
52 FINANZ_MINIMALIST 0.150893
48 FINANZTYP 0.113862
44 RETOURTYP_BK_S 0.097231
60 SEMIO_REL 0.086058
2 W_KEIT_KIND_HH 0.084028
53 FINANZ_VORSORGER 0.082199
61 SEMIO_MAT 0.065254
46 ALTERSKATEGORIE_GROB 0.057895
50 GREEN_AVANTGARDE 0.050881
63 SEMIO_LUST 0.048456
23 ORTSGR_KLS9 0.047664
26 EWDICHTE 0.045967
14 PLZ8_ANTG4 0.044667
9 PLZ8_ANTG3 0.044165
43 LP_STATUS_GROB 0.041108
21 ARBEIT 0.037737
22 RELAT_AB 0.031734
126 WEALTH 0.028790
10 PLZ8_ANTG2 0.028759
35 WOHNDAUER_2008 0.027823
42 LP_STATUS_FEIN 0.026335
79 CAMEO_DEUG_2015_9 0.025507
121 CAMEO_DEU_2015_9C 0.025164
6 KBA05_ANTG4 0.023331
20 ANZ_HAUSHALTE_AKTIV 0.022456
78 CAMEO_DEUG_2015_8 0.017940
... ... ...
18 NATIONALITAET_KZ -0.022304
39 HH_EINKOMMEN_SCORE -0.022377
8 MOBI_REGIO -0.024718
19 KBA13_ANZAHL_PKW -0.025106
74 CAMEO_DEUG_2015_4 -0.027769
28 GEBAEUDETYP_RASTER -0.029757
128 RURAL -0.031585
41 CJT_GESAMTTYP -0.032509
27 BALLRAUM -0.034150
34 KONSUMNAEHE -0.037033
11 PLZ8_GBZ -0.038475
25 INNENSTADT -0.043068
13 PLZ8_ANTG1 -0.045446
45 ONLINE_AFFINITAET -0.045756
125 MOVEMENTS -0.048722
49 FINANZ_HAUSBAUER -0.050849
69 SEMIO_TRADV -0.060304
58 SEMIO_PFLICHT -0.060321
70 ZABEOTYP -0.062217
47 FINANZ_UNAUFFAELLIGER -0.088339
124 DECADES -0.089237
51 FINANZ_SPARER -0.089568
17 SHOPPER_TYP -0.131343
54 FINANZ_ANLEGER -0.172378
64 SEMIO_ERL -0.188046
66 SEMIO_RAT -0.198265
67 SEMIO_KRIT -0.273300
68 SEMIO_DOM -0.307070
56 SEMIO_KAEM -0.332951
55 ANREDE_KZ -0.363639

130 rows × 2 columns

Discussion 2.3: Interpret Principal Components

(Double-click this cell and replace this text with your own text, reporting your observations from detailed investigation of the first few principal components generated. Can we interpret positive and negative values from them in a meaningful way?)

The first principal component is mainly about wealth and the typical building that familiies are living in. Top features like CAMEO_DEUG_2015, PLZ8_ANTG3, WEALTH, PLZ8_ANTG4 all about personal wealth or the great number of families living in one building. On the negative spectrum we have LP_STATUS_FEIN, FINANZ_MINIMALIST, KBA05_ANTG1 which indicates building with smaller capacities and the related financial typology.

The second principal component is mainly about age and personality . ALTERSKATEGORIE_GROB, FINANZ_VORSORGER, SEMIO_ERL, SEMIO_LUST are about age and personality characteristics. On the negative end we have SEMIO_TRADV, SEMIO_PFLICHT, FINANZ_SPARER, SEMIO_REL. As we age are personality changes as well, this principal component seems to be the mix of the two.

The third principal component all about personality and human characteristics.SEMIO_VERT, SEMIO_SOZ , SEMIO_FAM, SEMIO_KULT are about age and personality characteristics. On the negative end we have ANREDE_KZ, SEMIO_KAEM, SEMIO_DOM, SEMIO_KRIT which are all indicating a different personality.

Step 3: Clustering

Step 3.1: Apply Clustering to General Population

You've assessed and cleaned the demographics data, then scaled and transformed them. Now, it's time to see how the data clusters in the principal components space. In this substep, you will apply k-means clustering to the dataset and use the average within-cluster distances from each point to their assigned cluster's centroid to decide on a number of clusters to keep.

  • Use sklearn's KMeans class to perform k-means clustering on the PCA-transformed data.
  • Then, compute the average difference from each point to its assigned cluster's center. Hint: The KMeans object's .score() method might be useful here, but note that in sklearn, scores tend to be defined so that larger is better. Try applying it to a small, toy dataset, or use an internet search to help your understanding.
  • Perform the above two steps for a number of different cluster counts. You can then see how the average distance decreases with an increasing number of clusters. However, each additional cluster provides a smaller net benefit. Use this fact to select a final number of clusters in which to group the data. Warning: because of the large size of the dataset, it can take a long time for the algorithm to resolve. The more clusters to fit, the longer the algorithm will take. You should test for cluster counts through at least 10 clusters to get the full picture, but you shouldn't need to test for a number of clusters above about 30.
  • Once you've selected a final number of clusters to use, re-fit a KMeans instance to perform the clustering operation. Make sure that you also obtain the cluster assignments for the general demographics data, since you'll be using them in the final Step 3.3.
In [38]:
# Over a number of different cluster counts...
cluster_nums =  np.arange(1, 30, 5)
dist = []
    # run k-means clustering on the data and...
for clust in cluster_nums:
    kmeans = KMeans(clust)
    kmeans.fit(azdias_pca)
    # compute the average within-cluster distances.
    dist.append(-kmeans.score(azdias_pca))
In [39]:
# Investigate the change in within-cluster distance across number of clusters.
# HINT: Use matplotlib's plot function to visualize this relationship.
trace2 = go.Scatter(
    x = cluster_nums,
    y = dist,
    mode='lines+markers',

)

layout= dict(title = 'Average Distance Beteen Points and Cluster Center vs. Number of Clusters',
              xaxis = dict(title = 'Number of Clusters'),
              yaxis = dict(title = 'Average Distance'),
              )
data = [trace2]

fig = go.Figure(data=data, layout=layout)
    
iplot(fig)
In [40]:
# Re-fit the k-means model with the selected number of clusters and obtain
# cluster predictions for the general population demographics data.

kmeans = KMeans(20)
kmeans_best = kmeans.fit(azdias_pca)
azdias_impute['clusters'] = kmeans_best.predict(azdias_pca)

Discussion 3.1: Apply Clustering to General Population

(Double-click this cell and replace this text with your own text, reporting your findings and decisions regarding clustering. Into how many clusters have you decided to segment the population?)

Based on the plot of the KMeans results I've selected 20 clusters. There's no clear 'elbow' to the graph however the average distance does not decrease a lot after 20.

Step 3.2: Apply All Steps to the Customer Data

Now that you have clusters and cluster centers for the general population, it's time to see how the customer data maps on to those clusters. Take care to not confuse this for re-fitting all of the models to the customer data. Instead, you're going to use the fits from the general population to clean, transform, and cluster the customer data. In the last step of the project, you will interpret how the general population fits apply to the customer data.

  • Don't forget when loading in the customers data, that it is semicolon (;) delimited.
  • Apply the same feature wrangling, selection, and engineering steps to the customer demographics using the clean_data() function you created earlier. (You can assume that the customer demographics data has similar meaning behind missing data patterns as the general demographics data.)
  • Use the sklearn objects from the general demographics data, and apply their transformations to the customers data. That is, you should not be using a .fit() or .fit_transform() method to re-fit the old objects, nor should you be creating new sklearn objects! Carry the data through the feature scaling, PCA, and clustering steps, obtaining cluster assignments for all of the data in the customer demographics data.
In [41]:
# Load in the customer demographics data.
customers = pd.read_csv('Udacity_CUSTOMERS_Subset.csv', delimiter=';')
In [42]:
# Apply preprocessing, feature transformation, and clustering from the general
# demographics onto the customer data, obtaining cluster predictions for the
# customer demographics data.
customers_cln = clean_data(customers)

customer_feature_names = list(customers_cln.columns)

customers_impute = pd.DataFrame(imputer_model.transform(customers_cln), columns = customer_feature_names)

customers_scaled = pd.DataFrame(scalar_model.transform(customers_impute), columns = customer_feature_names)
#transform the customers data using pca object
customers_pca = pca.transform(customers_scaled)
#predict clustering using the kmeans object
customers_impute['clusters'] = kmeans_best.predict(customers_pca)

Step 3.3: Compare Customer Data to Demographics Data

At this point, you have clustered data based on demographics of the general population of Germany, and seen how the customer data for a mail-order sales company maps onto those demographic clusters. In this final substep, you will compare the two cluster distributions to see where the strongest customer base for the company is.

Consider the proportion of persons in each cluster for the general population, and the proportions for the customers. If we think the company's customer base to be universal, then the cluster assignment proportions should be fairly similar between the two. If there are only particular segments of the population that are interested in the company's products, then we should see a mismatch from one to the other. If there is a higher proportion of persons in a cluster for the customer data compared to the general population (e.g. 5% of persons are assigned to a cluster for the general population, but 15% of the customer data is closest to that cluster's centroid) then that suggests the people in that cluster to be a target audience for the company. On the other hand, the proportion of the data in a cluster being larger in the general population than the customer data (e.g. only 2% of customers closest to a population centroid that captures 6% of the data) suggests that group of persons to be outside of the target demographics.

Take a look at the following points in this step:

  • Compute the proportion of data points in each cluster for the general population and the customer data. Visualizations will be useful here: both for the individual dataset proportions, but also to visualize the ratios in cluster representation between groups. Seaborn's countplot() or barplot() function could be handy.
    • Recall the analysis you performed in step 1.1.3 of the project, where you separated out certain data points from the dataset if they had more than a specified threshold of missing values. If you found that this group was qualitatively different from the main bulk of the data, you should treat this as an additional data cluster in this analysis. Make sure that you account for the number of data points in this subset, for both the general population and customer datasets, when making your computations!
  • Which cluster or clusters are overrepresented in the customer dataset compared to the general population? Select at least one such cluster and infer what kind of people might be represented by that cluster. Use the principal component interpretations from step 2.3 or look at additional components to help you make this inference. Alternatively, you can use the .inverse_transform() method of the PCA and StandardScaler objects to transform centroids back to the original data space and interpret the retrieved values directly.
  • Perform a similar investigation for the underrepresented clusters. Which cluster or clusters are underrepresented in the customer dataset compared to the general population, and what kinds of people are typified by these clusters?
In [43]:
# Compare the proportion of data in each cluster for the customer data to the
# proportion of data in each cluster for the general population.
azdias_impute['row_count'] = 1
azdias_clusters_cnt = azdias_impute[['clusters', 'row_count']].groupby('clusters').count().reset_index()
azdias_clusters_cnt['ratio'] = azdias_clusters_cnt['row_count']/sum(azdias_clusters_cnt['row_count'])
azdias_clusters_cnt['dataset'] = 'General'

# customers demographics
customers_impute['row_count'] = 1
customers_clusters_cnt = customers_impute[['clusters', 'row_count']].groupby('clusters').count().reset_index()
customers_clusters_cnt['ratio'] = customers_clusters_cnt['row_count']/sum(customers_clusters_cnt['row_count'])
customers_clusters_cnt['dataset'] = 'Customers'
In [44]:
trace1 = go.Bar(
    x=azdias_clusters_cnt['clusters'],
    y=azdias_clusters_cnt['ratio'],
    name='General'
)
trace2 = go.Bar(
    x=customers_clusters_cnt['clusters'],
    y=customers_clusters_cnt['ratio'],
    name='Customers'
)

data = [trace1, trace2]
layout = dict(title = 'Cluster Visualizations for General and Customer Demographics',
              xaxis = dict(title = 'Clusters'),
              yaxis = dict(title = 'Percentage'),
              barmode='group'
              )

fig = go.Figure(data=data, layout=layout)
iplot(fig)
In [45]:
cluster_over_need = pd.DataFrame(customers_impute[customers_impute['clusters']==13].columns)
cluster_over_need.columns= ['columns']
In [46]:
# What kinds of people are part of a cluster that is overrepresented in the
# customer data compared to the general population?
13

cluster_over_need['most_frequent_value'] = cluster_over_need['columns'].apply(lambda col: 
                                                                              customers_impute[col][customers_impute['clusters']==13].mode().iloc[0])
In [47]:
cluster_over_need
Out[47]:
columns most_frequent_value
0 KKK 1.0
1 REGIOTYP 1.0
2 W_KEIT_KIND_HH 6.0
3 KBA05_ANTG1 4.0
4 KBA05_ANTG2 0.0
5 KBA05_ANTG3 0.0
6 KBA05_ANTG4 0.0
7 KBA05_GBZ 4.0
8 MOBI_REGIO 5.0
9 PLZ8_ANTG3 1.0
10 PLZ8_ANTG2 2.0
11 PLZ8_GBZ 5.0
12 PLZ8_HHZ 3.0
13 PLZ8_ANTG1 3.0
14 PLZ8_ANTG4 0.0
15 VERS_TYP 1.0
16 HEALTH_TYP 1.0
17 SHOPPER_TYP 3.0
18 NATIONALITAET_KZ 1.0
19 KBA13_ANZAHL_PKW 1400.0
20 ANZ_HAUSHALTE_AKTIV 1.0
21 ARBEIT 3.0
22 RELAT_AB 1.0
23 ORTSGR_KLS9 5.0
24 ANZ_HH_TITEL 0.0
25 INNENSTADT 4.0
26 EWDICHTE 5.0
27 BALLRAUM 2.0
28 GEBAEUDETYP_RASTER 4.0
29 OST_WEST_KZ 1.0
... ... ...
102 CAMEO_DEU_2015_5E 0.0
103 CAMEO_DEU_2015_5F 0.0
104 CAMEO_DEU_2015_6A 0.0
105 CAMEO_DEU_2015_6B 0.0
106 CAMEO_DEU_2015_6C 0.0
107 CAMEO_DEU_2015_6D 0.0
108 CAMEO_DEU_2015_6E 0.0
109 CAMEO_DEU_2015_6F 0.0
110 CAMEO_DEU_2015_7A 0.0
111 CAMEO_DEU_2015_7B 0.0
112 CAMEO_DEU_2015_7C 0.0
113 CAMEO_DEU_2015_7D 0.0
114 CAMEO_DEU_2015_7E 0.0
115 CAMEO_DEU_2015_8A 0.0
116 CAMEO_DEU_2015_8B 0.0
117 CAMEO_DEU_2015_8C 0.0
118 CAMEO_DEU_2015_8D 0.0
119 CAMEO_DEU_2015_9A 0.0
120 CAMEO_DEU_2015_9B 0.0
121 CAMEO_DEU_2015_9C 0.0
122 CAMEO_DEU_2015_9D 0.0
123 CAMEO_DEU_2015_9E 0.0
124 DECADES 50.0
125 MOVEMENTS 0.0
126 WEALTH 1.0
127 LIFE_STAGE 5.0
128 RURAL 0.0
129 NEIGHBORHOOD_QUAL 3.0
130 clusters 13.0
131 row_count 1.0

132 rows × 2 columns

In [48]:
# What kinds of people are part of a cluster that is underrepresented in the
# customer data compared to the general population?
12
cluster_under_need = pd.DataFrame(customers_impute[customers_impute['clusters']==12].columns)
cluster_under_need.columns= ['columns']
cluster_under_need['most_frequent_value'] = cluster_under_need['columns'].apply(lambda col: customers_impute[col][customers_impute['clusters']==12].mode().iloc[0])
In [49]:
cluster_under_need
Out[49]:
columns most_frequent_value
0 KKK 1.0
1 REGIOTYP 1.0
2 W_KEIT_KIND_HH 6.0
3 KBA05_ANTG1 1.0
4 KBA05_ANTG2 2.0
5 KBA05_ANTG3 0.0
6 KBA05_ANTG4 0.0
7 KBA05_GBZ 3.0
8 MOBI_REGIO 3.0
9 PLZ8_ANTG3 2.0
10 PLZ8_ANTG2 3.0
11 PLZ8_GBZ 3.0
12 PLZ8_HHZ 3.0
13 PLZ8_ANTG1 2.0
14 PLZ8_ANTG4 1.0
15 VERS_TYP 2.0
16 HEALTH_TYP 2.0
17 SHOPPER_TYP 3.0
18 NATIONALITAET_KZ 1.0
19 KBA13_ANZAHL_PKW 1400.0
20 ANZ_HAUSHALTE_AKTIV 1.0
21 ARBEIT 3.0
22 RELAT_AB 5.0
23 ORTSGR_KLS9 7.0
24 ANZ_HH_TITEL 0.0
25 INNENSTADT 2.0
26 EWDICHTE 6.0
27 BALLRAUM 1.0
28 GEBAEUDETYP_RASTER 4.0
29 OST_WEST_KZ 1.0
... ... ...
102 CAMEO_DEU_2015_5E 0.0
103 CAMEO_DEU_2015_5F 0.0
104 CAMEO_DEU_2015_6A 0.0
105 CAMEO_DEU_2015_6B 0.0
106 CAMEO_DEU_2015_6C 0.0
107 CAMEO_DEU_2015_6D 0.0
108 CAMEO_DEU_2015_6E 0.0
109 CAMEO_DEU_2015_6F 0.0
110 CAMEO_DEU_2015_7A 0.0
111 CAMEO_DEU_2015_7B 0.0
112 CAMEO_DEU_2015_7C 0.0
113 CAMEO_DEU_2015_7D 0.0
114 CAMEO_DEU_2015_7E 0.0
115 CAMEO_DEU_2015_8A 0.0
116 CAMEO_DEU_2015_8B 0.0
117 CAMEO_DEU_2015_8C 0.0
118 CAMEO_DEU_2015_8D 0.0
119 CAMEO_DEU_2015_9A 0.0
120 CAMEO_DEU_2015_9B 0.0
121 CAMEO_DEU_2015_9C 0.0
122 CAMEO_DEU_2015_9D 0.0
123 CAMEO_DEU_2015_9E 0.0
124 DECADES 50.0
125 MOVEMENTS 0.0
126 WEALTH 3.0
127 LIFE_STAGE 4.0
128 RURAL 0.0
129 NEIGHBORHOOD_QUAL 3.0
130 clusters 12.0
131 row_count 1.0

132 rows × 2 columns

Discussion 3.3: Compare Customer Data to Demographics Data

(Double-click this cell and replace this text with your own text, reporting findings and conclusions from the clustering analysis. Can we describe segments of the population that are relatively popular with the mail-order company, or relatively unpopular with the company?)

segments of the population that are relatively popular with the mail-order company is cluster 13 : most of them are and elderly people(between 40 and 60 years old), additionally this group can be considered in a positive financial status, living in a multiperson household, with a sufficient investment mindset and financial background to do so.

segments of the population that are relatively unpopular with the mail-order company is cluster 12: While the age is similar to the previously highlighted cluster, this group is from the lower middle class with poorer financial status, and with lack of investment fund and attitude

Congratulations on making it this far in the project! Before you finish, make sure to check through the entire notebook from top to bottom to make sure that your analysis follows a logical flow and all of your findings are documented in Discussion cells. Once you've checked over all of your work, you should export the notebook as an HTML document to submit for evaluation. You can do this from the menu, navigating to File -> Download as -> HTML (.html). You will submit both that document and this notebook for your project submission.